package com.checksound;

import java.text.DecimalFormat;
import java.util.concurrent.atomic.AtomicBoolean;

import soundanalyzer.*;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder.AudioSource;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class CheckSoundActivity extends Activity {

  //for pause/ resume facility
  AtomicBoolean isActive = new AtomicBoolean(true);
  
  //the recorder object
  AudioRecord recorder;
  
  //the sample rate of the recording
  static int fs = 8000;
  
  //parameter for the recorder object
  int bytesRead = 0;

  //for calculating the Fourier transform
  static final int N = 2048;
  
  //short array that pcm data is put into.
  short[] audioData = new short [N];

  //estimate the frequency
  float fBase = 0.0f;
  
  //frequency from the last cycle
  float previousFrequencyFromMethod0 = 0.0f;

  //array that holds the Fourier transform
  float[] ft;

  //multiplicative ratio between semitones: 2 ^ 1/12
  private static double r = 1.0594630943592953;

  //interface that analyzes the sound signal and calculates frequency and volume
  SoundAnalyzer soundAnalyzer = new SoundAnalyzer(fs, N);
  
  //main animation view
  private UiView mNoteLineView;
  
  //this boolean tells if any sound has been detected yet on the current run
  boolean noSoundDetectedYet;
  
  //a message telling the user to make a sound, which can be shown or hidden
  TextView makeSoundNotification;
  
  //a button that can be pressed to stop the current run and show the FFT
  Button stop;

  //value to hold the current calculated frequency, for use in displaying the UI, to avoid threading errors
  float currentFrequency;
  
  //precise (non-integer) value of note "number" as calculated by the expression log(frequency/440.0) / log(r), where r is a class-defined ratio
  float m_precise;
  
  //value of m_precise rounded to nearest integer
  int m;
  
  //difference between m_precise and m
  float nearestNoteOffset;
  
  //nearest note "index", where A=0, Bflat=1, ...
  int nearestNoteIndex;
  
  //frequency of nearest discrete semitone
  float nearestNoteFrequency;

  /**
   * onCreate method.
   */
  
  @Override
  public void onCreate(Bundle savedInstanceState)
  {  
	  super.onCreate(savedInstanceState);
  }

  /**
   * Initialize the default "CheckSound" layout, with the note line.
   */    
  
  private void initializeLayout()
  {
	  setContentView(R.layout.main);
	  
	  makeSoundNotification = (TextView) findViewById(R.id.makesound);

	  //initialize the "FFT" button, which stops the process and shows an FFT graph
	  stop=(Button)findViewById(R.id.stop);
	  stop.setOnClickListener(onStop);
  }

  /**
   * Function that is called when the "FFT" button is clicked: the default activity is paused and a the most recently-calculated Fourier transform is
   * is plotted as a graph.
   */  
  
  private View.OnClickListener onStop=new View.OnClickListener()
  {
	  public void onClick(View v)
	  {
		  isActive.set(false);

		  DecimalFormat df = new DecimalFormat("##.0");
		  
		  //plot the fourier transform
  		  //create an upper limit for the plot
  		  int iMax = ft.length;
		  double [] yValues = new double[iMax];
		  double [] xValues = new double[iMax];
		  for (int i = 0; i < iMax; i++) {
			  yValues[i] = (double) ft[i];
			  xValues[i] = (double) i * (double) fs/ N;
		  }

		  float volume = soundAnalyzer.calculateVolume(audioData);
		  
		  String graphTitle;
		  if (fBase > 0)
		  {
			  graphTitle = "Fourier Transform | " + df.format(fBase) + " Hz | " + df.format(volume) + " Db";
		  } else
		  {
			  graphTitle = "Fourier Transform | " + df.format(volume) + " Db";
		  }
	  	  
	  	  Intent intent = new LineChart(xValues, yValues, graphTitle, fBase).execute(CheckSoundActivity.this); 
	  	  startActivity(intent);		  

		  onPause();
	  }
  }; 
  
  /**
   * Start the work of recording the sound.
   */

  private void startWork()
  {
	  View containerView = (View)findViewById(R.id.container);
	  containerView.setBackgroundColor(Color.WHITE);
	  
	  //initialize recorder
	  int minBufferSize=AudioRecord.getMinBufferSize(fs,AudioFormat.CHANNEL_CONFIGURATION_MONO,
	  	  AudioFormat.ENCODING_PCM_16BIT); //get the buffer size to use with this audio record
	  if( minBufferSize > N*2 ){ // minBufferSize is in Bytes
		  Log.e("soundlog", "Bad min buffer size ");
	  	  return;
	  }
	  
	  recorder = new AudioRecord (AudioSource.MIC,fs,AudioFormat.CHANNEL_CONFIGURATION_MONO,
	  AudioFormat.ENCODING_PCM_16BIT,N); //instantiate the AudioRecorder

	  mNoteLineView = (UiView) findViewById(R.id.checksoundui);
	  
	  runOnUiThread(new Runnable(){
		public void run()
			{
			  //initialize the note view display at middle C, but with no note marker
			  mNoteLineView.updateParams(261.625565f, 261.625565f, 3, 0.0f, UiView.SOUND_STATUS_NONE);
			  mNoteLineView.postInvalidate();
			}
	  	});

	  //start the thread
	  isActive.set(true);
	  new Thread(recordSoundTask).start();
  }

  /**
   * Thread on which the work of reading in the sound and displaying data on screen will be done.
   */
  
  private Runnable recordSoundTask = new Runnable()
  {

	  /**
	   * Do the actual work of reading in sound and displaying data on screen.
	   *
	   * Loop over the number of iterations defined by the variable max and, on each iteration, analyze the data
	   * read in from the microphone & update the data displayed on screen.
	   * 
	   */
	  
	  public void run()
	  {
		  noSoundDetectedYet = true;
		  //on each iteration record sound and redraw the graphs
		  while (isActive.get())
		  {	  
			  //on each iteration, record the sound, calculate the frequency & redraw the graphs
			  recordSoundAndDoCalculations();
		  }
	  }
  };
  
  /**
   * 
   * Method to be called on each iteration of the run method.
   * 
   * Read in some data from the recorder and display the results in graphic form on screen. 
   */

  private void recordSoundAndDoCalculations()
  {
      if (recorder.getState()==android.media.AudioRecord.STATE_INITIALIZED) // check to see if the recorder has initialized yet.
      {
    	  if (recorder.getRecordingState()==android.media.AudioRecord.RECORDSTATE_STOPPED)
    	  {
    		  recorder.startRecording();  //check to see if the Recorder has stopped or is not recording, and make it record.               
    	  } else
    	  {
    	  	  bytesRead = recorder.read(audioData,0,N); //read the PCM audio data into the audioData array

    	  	  //calculate Fourier Transform
    	  	  ft = soundAnalyzer.calculateFourierTransform(audioData);
    	  	  
    	  	  previousFrequencyFromMethod0 = fBase;
	    	  fBase = soundAnalyzer.calculateFundamentalFrequency();

	    	  if( fBase == 0 )
	    	  {
	    		  //some logic controlling the view based on whether we have a valid sound or not
	    		  if ((previousFrequencyFromMethod0 != fBase) || noSoundDetectedYet)
	    		  {
		    		  runOnUiThread(new Runnable(){
			  			public void run()
			  				{
			  				    //if the current status of the view is valid, update it to invalid
			  					if (mNoteLineView.getSoundStatus() == UiView.SOUND_STATUS_VALID)
			  					{
			  						 mNoteLineView.setSoundStatus(UiView.SOUND_STATUS_INVALID);
			  						 if (isActive.get())
			  						 {
			  							 //redraw view
			  							 mNoteLineView.postInvalidate();
			  						 }
			  					} else if (noSoundDetectedYet && (mNoteLineView.getSoundStatus() == UiView.SOUND_STATUS_NONE))
			  		    		{
			  						//no sound has yet been detected
		  							//show a message telling the user to make a sound
			  						makeSoundNotification.setVisibility(View.VISIBLE);
			  						noSoundDetectedYet = false;
			  		    		}
			  			    }
		    		  });
	    		  }

	    		//we don't have a valid sound so we don't need to do any more work here
	    	    return;
	    	    
	    	  }
	    	  
		      //if a valid frequency has been detected, update the view
			  runOnUiThread(new Runnable(){
				  public void run()
				  {
					  currentFrequency = fBase;
					  if (currentFrequency > 0.0) //use this extra check because of thread issues
					  {
						//if this is the first valid sound that has been detected, hide the dialog that tells the user to make a sound
						if (mNoteLineView.getSoundStatus() == UiView.SOUND_STATUS_NONE)
						{
							makeSoundNotification.setVisibility(View.GONE);
			    		    noSoundDetectedYet = false;
						}

				        m_precise = (float) (Math.log(((double) currentFrequency/440.0)) / Math.log(r));

				        //Log.v("soundlog", "frequency=" + currentFrequency);
				        //Log.v("soundlog", "m_precise=" + m_precise);
				        
				        //nearest semitone
				        m = (int) Math.round(m_precise);
				        //calculate the distance between m_precise and m, the nearest semitone
				        nearestNoteOffset = m_precise - (float) m;
				        
				        //Log.v("soundlog", "m=" + m);
				        //Log.v("soundlog", "nearestNoteOffset=" + nearestNoteOffset);
				        
				        //calculate the nearest semitone index
				        nearestNoteIndex = (m+1200) % 12;
				        
				        //Log.v("soundlog", "nearestNoteIndex=" + nearestNoteIndex);
				        
				        //calculate the frequency of the nearest note for display purposes
				        nearestNoteFrequency = 440.0f * (float) Math.pow(r, m);
				        
				        //Log.v("soundlog", "nearestNoteFrequency=" + nearestNoteFrequency);

						if (isActive.get()) //use this extra check because of thread issues
						{
							mNoteLineView.updateParams((float) currentFrequency, (float) nearestNoteFrequency, nearestNoteIndex, (float) nearestNoteOffset, UiView.SOUND_STATUS_VALID);

							//redraw view
							mNoteLineView.postInvalidate();
						}
					 }
				  }
			  });
		  
    	  } //else recorder started
	  
      } //if recorder initialized
  }

  /**
   * 
   * Method called when the activity is paused.
   * 
   * Stop recording and re-initialize so the user starts from the beginning next time.
   * 
   */
  
  @Override
  public void onPause()
  {
	  Log.v("soundlog", "called onPause");
	  isActive.set(false);
	  
	  super.onPause();

	  //stop recording
      if (recorder != null)
      {
    	  recorder.stop(); //stop the recorder before ending the thread
          recorder.release(); //release the recorders resources
      }

      makeSoundNotification.setVisibility(View.GONE);
  
      recorder=null; //set the recorder to be garbage collected.
  }
  
  /**
   * 
   * Method called when the activity is restarted.
   * 
   * Re-initialize so the user starts from the beginning.
   * 
   */
  
  @Override
  public void onResume()
  {
	  Log.v("soundlog", "called onResume");
	  
	  super.onResume();
	  
	  initializeLayout();
	  startWork();
  }

}